Skip to content

docs: add otlp.cjs helper and guide for emitting custom OTLP attributes from shared workflows#29623

Merged
pelikhan merged 9 commits intomainfrom
copilot/update-documentation-otlp-attributes
May 1, 2026
Merged

docs: add otlp.cjs helper and guide for emitting custom OTLP attributes from shared workflows#29623
pelikhan merged 9 commits intomainfrom
copilot/update-documentation-otlp-attributes

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented May 1, 2026

Adds a stable, service-level JavaScript helper (otlp.cjs) and a new documentation guide that explains how shared agentic workflow imports can emit additional OpenTelemetry spans and attributes. This enables third-party tools (APM agents, scanners, data pipeline steps) to upload their own telemetry alongside gh-aw's built-in instrumentation, all correlated under the same distributed trace.

Changes

  • actions/setup/js/otlp.cjs — new high-level helper exposing a single logSpan(toolName, attributes, options) function:
    • Reads OTEL_EXPORTER_OTLP_ENDPOINT, GITHUB_AW_OTEL_TRACE_ID, and GITHUB_AW_OTEL_PARENT_SPAN_ID from the environment automatically — no configuration needed by callers
    • Requires shim.cjs at load time so global.core and global.context are always available, whether the module is loaded inside github-script or as a plain Node.js process
    • Wraps send_otlp_span.cjs internally, keeping the public surface minimal and stable across future refactors
    • Non-fatal: export failures are surfaced as console.warn and never throw
    • Silently skips (no warning) when GITHUB_AW_OTEL_TRACE_ID is missing or invalid — zero noise in environments where OTLP tracing is not configured
    • Applies sanitizeOTLPPayload before writing to the local JSONL mirror so sensitive attribute values are redacted in both the over-the-wire export and the debug artifact
    • Skips the HTTP export entirely when OTEL_EXPORTER_OTLP_ENDPOINT is not set; the sanitized JSONL mirror is always written regardless
  • actions/setup/js/otlp.test.cjs — 16 unit tests covering happy path, attribute conversion, status codes, options overrides, silent skip on missing trace ID, error resilience, absent parent span ID handling, shim integration, JSONL sanitization, and the "endpoint not set" path (no HTTP export attempted, JSONL still written)
  • docs/src/content/docs/guides/custom-otlp-attributes.md — new guide covering:
    • Quick-start example using otlp.logSpan() from a shared import's steps: entry
    • Full logSpan API reference table
    • Attribute naming recommendations aligned with OTel semantic conventions
    • Security section: sanitization (redaction + truncation) is applied to both the wire export and the local JSONL mirror
    • Debugging with the /tmp/gh-aw/otel.jsonl JSONL mirror artifact (always sanitized)
    • Advanced section: low-level send_otlp_span.cjs API for multiple linked spans and resource attributes
  • docs/astro.config.mjs — adds "Custom OTLP Attributes" entry to the Guides sidebar section
  • docs/src/content/docs/reference/qmd.md — adds a Telemetry section with a shared import example that emits qmd.index.size (documents in the qmd index) and qmd.search.hits (cumulative search result count) via otlp.logSpan; also links the new Custom OTLP Attributes guide from the Related Documentation section

Copy link
Copy Markdown
Collaborator

@pelikhan pelikhan left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot create a helper file, tlp.cgs, that users can require. It should provide a stable, service-level API for OpenTelemetry with minimal configuration compared to the samples here. The user should be able to log attributes with a tool name, and the library should automatically handle environment variables and all underlying machinery. Users shouldn’t need to manage any of that. The goal is to expose a very small public function surface while keeping the internals flexible so they can be refactored over time.

Copy link
Copy Markdown
Contributor Author

Copilot AI commented May 1, 2026

@copilot create a helper file, tlp.cgs, that users can require. It should provide a stable, service-level API for OpenTelemetry with minimal configuration compared to the samples here. The user shou...

Done in commit 927c3f6. Added actions/setup/js/otlp.cjs with a single logSpan(toolName, attributes, options) function — users provide a tool name and a flat attribute map; the helper resolves the endpoint, trace ID, and parent span ID from the environment automatically. The full send_otlp_span.cjs API is kept as an "Advanced" section in the guide for callers that need multiple linked spans or resource attributes. 13 unit tests cover the happy path, missing trace ID early-return, error resilience, and parent span ID handling.

Copilot AI changed the title docs: add guide for emitting custom OTLP attributes from shared workflows docs: add otlp.cjs helper and guide for emitting custom OTLP attributes from shared workflows May 1, 2026
Copilot AI requested a review from pelikhan May 1, 2026 20:54
@pelikhan
Copy link
Copy Markdown
Collaborator

pelikhan commented May 1, 2026

@copilot Require shims.cjs and otlp.cjs to get the shims for core and other action-specific JavaScript.

…ontext are available

Agent-Logs-Url: https://github.com/github/gh-aw/sessions/048292c0-1229-4789-a893-5e967b10c76f

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Copy link
Copy Markdown
Contributor Author

Copilot AI commented May 1, 2026

@copilot Require shims.cjs and otlp.cjs to get the shims for core and other action-specific JavaScript.

Done in commit ddbf503. otlp.cjs now calls require(path.join(__dirname, "shim.cjs")) at load time, following the same pattern used by substitute_placeholders.cjs, render_template.cjs, etc. This ensures global.core and global.context are always populated when the module is loaded outside the github-script runtime. A new shim integration test confirms global.core.warning and global.core.info are available after the module loads.

Comment thread actions/setup/js/otlp.cjs Outdated
// ---------------------------------------------------------------------------

/** @type {typeof import('./send_otlp_span.cjs') | null} */
let _core = null;
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot first, core is a reserved namespace and should not be used. Second, do not capture the result of a call to require; it is already cached by Node.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in commit 39dfff0. Removed the _core variable and the core() helper function entirely. logSpan now calls require(path.join(__dirname, "send_otlp_span.cjs")) inline — Node's module cache ensures the module is only evaluated once regardless of how many times require is called.

Copilot AI requested a review from pelikhan May 1, 2026 21:10
@pelikhan pelikhan marked this pull request as ready for review May 1, 2026 21:19
Copilot AI review requested due to automatic review settings May 1, 2026 21:19
@github-actions github-actions Bot mentioned this pull request May 1, 2026
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 1, 2026

🧪 Test Quality Sentinel Report

Test Quality Score: 81/100

Excellent test quality

Metric Value
New/modified tests analyzed 14
✅ Design tests (behavioral contracts) 14 (100%)
⚠️ Implementation tests (low value) 0 (0%)
Tests with error/edge cases 5 (36%)
Duplicate test clusters 0
Test inflation detected No (2.0:1 — exactly at threshold)
🚨 Coding-guideline violations None

Test Classification Details

View All 14 Test Classifications
Test File Classification Notes
populates global.core when otlp.cjs is loaded otlp.test.cjs ✅ Design Verifies shim integration contract
calls sendOTLPSpan with payload including tool name as service name otlp.test.cjs ✅ Design Verifies serviceName/spanName mapping contract
uses the trace ID from GITHUB_AW_OTEL_TRACE_ID otlp.test.cjs ✅ Design Verifies env-var-to-payload binding
uses the parent span ID from GITHUB_AW_OTEL_PARENT_SPAN_ID otlp.test.cjs ✅ Design Verifies env-var-to-payload binding
reads the endpoint from OTEL_EXPORTER_OTLP_ENDPOINT otlp.test.cjs ✅ Design Verifies endpoint resolution contract
converts attributes object to buildAttr calls otlp.test.cjs ✅ Design Verifies attribute mapping contract
uses statusCode 1 (OK) by default otlp.test.cjs ✅ Design Verifies default status code
uses statusCode 2 (ERROR) when isError is true otlp.test.cjs ✅ Design Error path + errorMessage propagation
accepts options.traceId override otlp.test.cjs ✅ Design Verifies override contract
accepts options.endpoint override otlp.test.cjs ✅ Design Verifies override contract
warns and skips the span when GITHUB_AW_OTEL_TRACE_ID is not set otlp.test.cjs ✅ Design Missing-env-var guard
does not throw when sendOTLPSpan rejects otlp.test.cjs ✅ Design Network-failure resilience contract
does not throw when an internal helper throws synchronously otlp.test.cjs ✅ Design Sync-error resilience contract
omits parentSpanId when GITHUB_AW_OTEL_PARENT_SPAN_ID is not set otlp.test.cjs ✅ Design Optional-field guard

Flagged Tests — Requires Review

No tests flagged. All tests enforce behavioral contracts of the logSpan facade.

Note on mocking strategy: The tests patch send_otlp_span.cjs exports via the shared CJS module cache (sendOtlpModule.buildAttr = mockFn). This is a legitimate approach — sendOTLPSpan makes real HTTP calls (external I/O), so isolating it is appropriate. Since logSpan returns Promise<void>, asserting on what was passed to the underlying network layer is the primary observable behavior available.


Language Support

Tests analyzed:

  • 🐹 Go (*_test.go): 0 tests
  • �� JavaScript (*.test.cjs): 14 tests (vitest)

Score Breakdown

Component Score
Behavioral Coverage (14/14 design tests × 40) 40/40
Error/Edge Case Coverage (5/14 × 30) ~11/30
Low Duplication (0 duplicate clusters) 20/20
Proportional Growth (2.0:1 ratio — at limit, not over) 10/10
Total 81/100

Verdict

Check passed. 0% of new tests are implementation tests (threshold: 30%). The otlp.cjs module's behavioral contracts — env-var resolution, attribute mapping, error status, override options, and error resilience — are all covered.


📖 Understanding Test Classifications

Design Tests (High Value) verify what the system does:

  • Assert on observable outputs, return values, or state changes
  • Cover error paths and boundary conditions
  • Would catch a behavioral regression if deleted
  • Remain valid even after internal refactoring

Implementation Tests (Low Value) verify how the system does it:

  • Assert on internal function calls (mocking internals)
  • Only test the happy path with typical inputs
  • Break during legitimate refactoring even when behavior is correct
  • Give false assurance: they pass even when the system is wrong

Goal: Shift toward tests that describe the system's behavioral contract — the promises it makes to its users and collaborators.

🧪 Test quality analysis by Test Quality Sentinel · ● 411.4K ·

Copy link
Copy Markdown
Contributor

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Test Quality Sentinel: 81/100. Test quality is excellent — 0% of new tests are implementation tests (threshold: 30%). All 14 tests in otlp.test.cjs verify behavioral contracts of the logSpan facade.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a stable JavaScript helper (otlp.cjs) plus documentation describing how shared agentic workflow imports can emit custom OTLP spans/attributes correlated to the same distributed trace as gh-aw’s built-in instrumentation.

Changes:

  • Introduces actions/setup/js/otlp.cjs with a single logSpan(toolName, attributes, options) API that reads trace context + endpoint from env and is non-fatal on export failures.
  • Adds actions/setup/js/otlp.test.cjs unit tests for logSpan behavior (trace/parent handling, attribute conversion, status, overrides, resilience, shim integration).
  • Adds a new documentation guide and links it in the Guides sidebar.
Show a summary per file
File Description
docs/src/content/docs/guides/custom-otlp-attributes.md New guide explaining how to emit custom OTLP spans/attributes from shared workflow imports.
docs/astro.config.mjs Adds the new guide to the Guides sidebar.
actions/setup/js/otlp.cjs New high-level OTLP helper wrapping send_otlp_span.cjs with a minimal public API.
actions/setup/js/otlp.test.cjs New Vitest unit tests covering the helper’s key behaviors and failure handling.

Copilot's findings

Tip

Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comments suppressed due to low confidence (2)

actions/setup/js/otlp.cjs:86

  • When OTEL_EXPORTER_OTLP_ENDPOINT is unset, endpoint becomes an empty string and sendOTLPSpan will still attempt a fetch to "/v1/traces" (which will warn/retry with an invalid URL). If the goal is “debugging without a live collector”, consider short-circuiting when endpoint is empty: still write the JSONL mirror, but skip the HTTP export entirely to avoid noisy warnings and retry delays.
    const endpoint = options.endpoint ?? process.env.OTEL_EXPORTER_OTLP_ENDPOINT ?? "";
    const traceId = options.traceId ?? process.env.GITHUB_AW_OTEL_TRACE_ID ?? "";
    const parentSpanId = options.parentSpanId ?? process.env.GITHUB_AW_OTEL_PARENT_SPAN_ID ?? "";

    if (!isValidTraceId(traceId)) {
      console.warn(`[otlp] ${toolName}: GITHUB_AW_OTEL_TRACE_ID is not set or invalid; skipping span`);

docs/src/content/docs/guides/custom-otlp-attributes.md:116

  • The guide says /tmp/gh-aw/otel.jsonl is included in the firewall-audit-logs artifact and suggests gh aw logs <run-id> --artifacts firewall, but the artifacts reference shows firewall-audit-logs contains firewall/audit logs only (no otel.jsonl). The OTLP mirror is part of the unified agent artifact when OTLP is enabled, so the download instructions/artifact name here should be updated accordingly (and note where the file lands under the logs output directory).
Every span is always appended as a JSON line to `/tmp/gh-aw/otel.jsonl`, even when `OTEL_EXPORTER_OTLP_ENDPOINT` is not set. This file is included in the `firewall-audit-logs` artifact so you can inspect spans after the run:

```bash
# Download firewall/telemetry artifacts for a run
gh aw logs <run-id> --artifacts firewall

# Inspect spans emitted by your tool
cat otel.jsonl | jq 'select(.resourceSpans[].scopeSpans[].spans[].name | startswith("my-tool"))'
</details>


- **Files reviewed:** 4/4 changed files
- **Comments generated:** 3


Comment on lines +99 to +108
All attribute values are sanitized before the payload is sent over the wire:

- **Redacts** the value of any attribute whose key matches `token`, `secret`, `password`, `passwd`, `key`, `auth`, `credential`, `api-key`, or `access-key` (case-insensitive), replacing it with `[REDACTED]`.
- **Truncates** string values longer than 1,024 characters.

Sanitization is automatic — you do not need to call it yourself.

## Debugging without a live collector

Every span is always appended as a JSON line to `/tmp/gh-aw/otel.jsonl`, even when `OTEL_EXPORTER_OTLP_ENDPOINT` is not set. This file is included in the `firewall-audit-logs` artifact so you can inspect spans after the run:
Copy link

Copilot AI May 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This section implies sensitive attribute redaction/truncation is automatic, but the local JSONL mirror (otel.jsonl) is currently written with the raw payload (before sanitizeOTLPPayload is applied) in send_otlp_span.cjs. That means secrets can still end up in the artifact even though the over-the-wire export is sanitized. Either clarify in the docs that the JSONL mirror is not sanitized, or adjust otlp.logSpan to write a sanitized copy to the mirror (e.g., use sanitizeOTLPPayload + appendToOTLPJSONL and call sendOTLPSpan with skipJSONL: true).

This issue also appears on line 108 of the same file.

Suggested change
All attribute values are sanitized before the payload is sent over the wire:
- **Redacts** the value of any attribute whose key matches `token`, `secret`, `password`, `passwd`, `key`, `auth`, `credential`, `api-key`, or `access-key` (case-insensitive), replacing it with `[REDACTED]`.
- **Truncates** string values longer than 1,024 characters.
Sanitization is automatic — you do not need to call it yourself.
## Debugging without a live collector
Every span is always appended as a JSON line to `/tmp/gh-aw/otel.jsonl`, even when `OTEL_EXPORTER_OTLP_ENDPOINT` is not set. This file is included in the `firewall-audit-logs` artifact so you can inspect spans after the run:
Attribute values are sanitized automatically before the OTLP payload is sent over the wire:
- **Redacts** the value of any attribute whose key matches `token`, `secret`, `password`, `passwd`, `key`, `auth`, `credential`, `api-key`, or `access-key` (case-insensitive), replacing it with `[REDACTED]`.
- **Truncates** string values longer than 1,024 characters.
You do not need to call sanitization yourself for OTLP export. However, if you use the local JSONL mirror for debugging, do not assume it is redacted — avoid putting secrets or other sensitive values into custom attributes.
## Debugging without a live collector
Every span is always appended as a JSON line to `/tmp/gh-aw/otel.jsonl`, even when `OTEL_EXPORTER_OTLP_ENDPOINT` is not set. This local debug file is included in the `firewall-audit-logs` artifact so you can inspect spans after the run, but it should be treated as potentially containing raw attribute values rather than a sanitized/redacted copy:

Copilot uses AI. Check for mistakes.
Comment thread actions/setup/js/otlp.cjs Outdated
Comment on lines +75 to +90
const { buildAttr, buildOTLPPayload, sendOTLPSpan, generateSpanId, isValidTraceId, isValidSpanId, SPAN_KIND_CLIENT } = require(path.join(__dirname, "send_otlp_span.cjs"));

const now = Date.now();
const startMs = options.startMs ?? now;
const endMs = options.endMs ?? now;

const endpoint = options.endpoint ?? process.env.OTEL_EXPORTER_OTLP_ENDPOINT ?? "";
const traceId = options.traceId ?? process.env.GITHUB_AW_OTEL_TRACE_ID ?? "";
const parentSpanId = options.parentSpanId ?? process.env.GITHUB_AW_OTEL_PARENT_SPAN_ID ?? "";

if (!isValidTraceId(traceId)) {
console.warn(`[otlp] ${toolName}: GITHUB_AW_OTEL_TRACE_ID is not set or invalid; skipping span`);
return;
}

const spanAttrs = Object.entries(attributes).map(([k, v]) => buildAttr(k, v));
Copy link

Copilot AI May 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sendOTLPSpan appends the raw payload to otel.jsonl before sanitization, so using it directly here can leak sensitive attributes into the local mirror artifact. If the intention is that otlp.logSpan is safe-by-default (as the docs suggest), consider mirroring a sanitized payload (via sanitizeOTLPPayload + appendToOTLPJSONL) and then calling sendOTLPSpan(..., { skipJSONL: true }) for the network export.

This issue also appears on line 81 of the same file.

Copilot uses AI. Check for mistakes.

expect(mockSendOTLPSpan).toHaveBeenCalledWith("https://otel.example.com", expect.anything());
});

Copy link

Copilot AI May 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There’s no unit test covering the “endpoint not set” path. Since the guide explicitly calls out debugging without a live collector, it would be good to add a test that deletes OTEL_EXPORTER_OTLP_ENDPOINT and asserts the expected behavior (e.g., no HTTP export attempt / no warning spam, while still producing the JSONL mirror if that’s intended).

Suggested change
it("does not attempt HTTP export when OTEL_EXPORTER_OTLP_ENDPOINT is not set", async () => {
delete process.env.OTEL_EXPORTER_OTLP_ENDPOINT;
await otlp.logSpan("my-scanner", {});
expect(mockSendOTLPSpan).not.toHaveBeenCalled();
});

Copilot uses AI. Check for mistakes.
@pelikhan
Copy link
Copy Markdown
Collaborator

pelikhan commented May 1, 2026

@copilot secret redaction and sanitization should be applied to attribute values

@pelikhan
Copy link
Copy Markdown
Collaborator

pelikhan commented May 1, 2026

@copilot review all comments

Copy link
Copy Markdown
Contributor Author

Copilot AI commented May 1, 2026

@copilot secret redaction and sanitization should be applied to attribute values

Fixed in commit fca2491. logSpan now calls sanitizeOTLPPayload(payload) before passing the result to appendToOTLPJSONL, so the local JSONL debug mirror gets the same redacted/truncated payload that is sent over the wire. sendOTLPSpan is called with { skipJSONL: true } to avoid a double write.

@pelikhan
Copy link
Copy Markdown
Collaborator

pelikhan commented May 1, 2026

@copilot needs to silently skip if environement variables are missing

… written; update docs

Agent-Logs-Url: https://github.com/github/gh-aw/sessions/71150d30-6996-41ac-9437-d6062c8168cb

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Copy link
Copy Markdown
Contributor Author

Copilot AI commented May 1, 2026

@copilot review all comments

Done in commit 36d5fe4. Addressed all open reviewer comments:

  • JSONL mirror is now sanitized (same redaction as wire export) — sanitizeOTLPPayload applied before appendToOTLPJSONL
  • HTTP export skipped when OTEL_EXPORTER_OTLP_ENDPOINT is unset; JSONL is still always written
  • Test added for "endpoint not set" path
  • Docs Security/Debugging section updated to reflect both paths are now sanitized

Copy link
Copy Markdown
Contributor Author

Copilot AI commented May 1, 2026

@copilot needs to silently skip if environement variables are missing

Done in commit d5666cc. logSpan now returns silently (no console.warn) when GITHUB_AW_OTEL_TRACE_ID is missing or invalid. The test was updated to assert no warning is emitted in that path.

@pelikhan
Copy link
Copy Markdown
Collaborator

pelikhan commented May 1, 2026

@copilot review and make sure docs are up to date. Use tech-writer agent

@pelikhan
Copy link
Copy Markdown
Collaborator

pelikhan commented May 1, 2026

@copilot instrument qmd.md with otlp for index size, hits

…sing trace ID

Agent-Logs-Url: https://github.com/github/gh-aw/sessions/03160233-1e86-4379-ae68-df738ddd0f87

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Copy link
Copy Markdown
Contributor Author

Copilot AI commented May 1, 2026

@copilot review and make sure docs are up to date. Use tech-writer agent

Done in commit 8c0c844. The only stale text was in the logSpan API section — it still said invalid trace IDs produce a console.warn. Updated to reflect the current behavior: missing/invalid trace ID silently skips with no side-effects; only actual export failures still emit a warning.

Copy link
Copy Markdown
Contributor Author

Copilot AI commented May 1, 2026

@copilot instrument qmd.md with otlp for index size, hits

Done in commit 039a0e3. Added a Telemetry section to docs/src/content/docs/reference/qmd.md with a shared import example that reads qmd.index.size from /tmp/gh-aw/qmd/stats.json and sums qmd.search.hits from /tmp/gh-aw/qmd/queries.jsonl, then emits both via otlp.logSpan('qmd', ...). A follow-up aw code block shows how to import the shared file alongside observability-otlp.md. Also linked the Custom OTLP Attributes guide from the Related Documentation section.

@pelikhan pelikhan merged commit b204b81 into main May 1, 2026
@pelikhan pelikhan deleted the copilot/update-documentation-otlp-attributes branch May 1, 2026 22:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants